<?php
/*
 * Copyright 2007 Bjørn Tore Håvie
 * 
 * This file is part of ITSBOT.
 * 
 * ITSBOT is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ITSBOT is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ITSBOT.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * @package ITSBOT
 * @subpackage Main
 * @license http://www.gnu.org/licenses/gpl.txt GNU General Public License
 */

/**
 * The main bot class. Currently also handles the IRC protocol.
 *
 */
class Bot{

	/**
	 * The instance
	 *
	 * @var Bot
	 */
	private static $inst;

	/**
	 * Socket
	 *
	 * @var Socket
	 */
	private $socket;

	/**
	 * The log
	 *
	 * @var Log
	 */
	private $log;

	/**
	 * mySQL
	 *
	 * @var mySQL
	 */
	private $db;

	/**
	 * The timer
	 *
	 * @var Timer
	 */
	private $timer;

	private $nick;

	private $isConnected = false;

	private $channels = array();

	private $modules = array();

	private $commands = array();

	private $hooks = array();

	private $users = array();

	private $run = true;

	/**
	 * Get the instance of the bot
	 *
	 * @return Bot
	 */
	public static function Get(){
		if(Bot::$inst)
			return Bot::$inst;
		Bot::$inst = new Bot();
			return Bot::$inst;
	}

	private function __construct(){
		$this->log = Log::Get();
		$this->timer = Timer::Get();
	}

	/**
	 * Initialize the bot and connect to server
	 *
	 * @param string $nick
	 * @param string $host
	 * @param int $port
	 * @param mySQL $db
	 */
	public function Init($nick, $host, $port, $db = null){
		$this->socket = new Socket($host, $port);
		$this->socket->SetBlocking(false);
		$this->db = $db;
		$this->Send("USER PHPBOT localhost $host :Foo Bar");
		$this->Nick($nick);
	}

	/**
	 * Load modules
	 *
	 * @param string $dir
	 */
	public function LoadModules($dir){
		$dire = opendir($dir);
		while($d = readdir($dire)){
			if($d == '.' || $d ==  '..' || is_file("$dir/$d") || !file_exists("$dir/$d/mod.php")) continue;
			$this->log->Notice("Registering module $d");
			include("$dir/$d/mod.php");
			if(class_exists($d))
				$this->modules[$d] = new $d($this->db);
			else
				$this->log->Warning("Class $d not found!");
		}
		foreach($this->modules as $m) if(method_exists($m, 'Init')) $m->Init();
	}

	/**
	 * Enter description here...
	 *
	 * @param string $hook
	 * @param mixed $arg
	 */
	private function RunHook($hook, $arg = null){
		if(!isset($this->hooks[$hook])) return true;
		$ret = true;
		foreach($this->hooks[$hook] as $func){
			$ret = $ret && call_user_func($func, $arg);
		}
		return $ret;
	}

	public function RegisterHook($name, $func){
		if(!is_callable($func)){
			$this->log->Error("Failed registerng hook, $func isn't callable!");
			return false;
		}
		$this->hooks[$name][] = $func;
		return true;
	}

	/**
	 * Start the main loop
	 *
	 */
	public function Run(){
		while($this->run){
			$data = $this->socket->Read(2048);
			array_map(array($this, 'HandleResponse'), explode("\r\n", $data));
			//$this->HandleResponse($data);
			$this->timer->Wait(1, 0);
		}

		$this->log->Notice('Execution ended');

	}

	/**
	 * Handle the message from the server. Basically just a huge switch-case
	 *
	 * @param string $data
	 */
	private function HandleResponse($data){
		if(empty($data)) return;
		$res = $this->ParseResponse($data);
		switch($res['COM']){
			case 'PING':
				$this->Send("PONG {$res['PARAM']}");
				break;
			case 'NOTICE':
				$this->RunHook('Notice', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
				break;
			case 'PRIVMSG':
				if(substr($res['PARAM'], 0, 1) == COM){
					$sp = $this->ParseCommand($res['PARAM']);
					$this->Command($sp[0], new Args($sp[1], $res['ORI'], $res['HOST'], $res['TAR']));
				} else if(ord($res['PARAM']) == 0x01) {
					$this->RunHook('OnCTCP', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
				} else {
					$this->RunHook('Message', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
				}
				break;
			case 'ACTION':
				$this->RunHook('Action', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
				break;
			case 'MODE':
				if(!$this->isConnected){
					$this->RunHook('Connected', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
					$this->isConnected = true;
					array_map(array($this, 'Join'), $this->channels);
				}
				$this->RunHook('Mode', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
				break;
			case 'JOIN':
				$this->RunHook('Join', new Args($res['PARAM'], $res['ORI'], $res['HOST'], $res['TAR']));
				break;
			case 'USER':
				print_r($res);
				break;
			default:
				if(!is_numeric($res['COM']) && $res['COM'] != '')
					$this->log->Warning("Unhandled command {$res['COM']}!");
				break;
		}
	}

	/**
	 * Send a message to a channel or a user
	 *
	 * @param string $target
	 * @param string $msg
	 */
	public function Say($target, $msg){
		$this->Send("PRIVMSG $target :$msg");
	}
	
	/**
	 * Send a notice
	 *
	 * @param string $target
	 * @param string $msg
	 */
	public function Notice($target, $msg){
		$this->Send("NOTICE $target :$msg");
	}

	/**
	 * Join a channel
	 *
	 * @param string $chan
	 */
	public function Join($chan){
		$this->Send("JOIN $chan");
		$this->log->Notice("Joining channel $chan");
		$channels[] = $chan;
	}

	/**
	 * Change nick
	 *
	 * @param string $nick
	 */
	public function Nick($nick){
		$this->Send("NICK $nick");
		$this->nick = $nick;
		$this->log->Notice("Changing nick to $nick");
	}

	/**
	 * Send a raw message
	 */
	private function Send($msg){
		$this->socket->Send("$msg\r\n");
	}

	/**
	 * Call a command
	 *
	 * @param string $command
	 * @param Args $arg
	 */
	private function Command($command, $arg){
		if($this->RunHook('BeforeCommand', array($command, $arg)) && is_callable($this->commands[$command])){
			$this->log->Notice("Calling command $command");
			if(call_user_func($this->commands[$command], $arg) === false)
				$this->log->Error("Failed to call command $command!");
		}
		else{
			$this->log->Notice("Unknown command $command");
			$this->RunHook('UnknownCommand', array($com, $arg));
		}
	}


	/**
	 * Register a command
	 *
	 * @param string $command
	 * @param callback $func
	 */
	public function RegisterCommand($command, $func){
		$this->commands[$command] = $func;
	}

	/**
	 * Disconnect
	 *
	 * @param string $msg
	 */
	public function Quit($msg = 'Bye'){
		$this->Send("QUIT :$msg");
		$this->run = false;
	}

	/**
	 * Parse the command
	 *
	 * @param string $msg
	 * @return array
	 */
	private function ParseCommand($msg){
		$msg = str_replace(array("\n", "\r"), '', $msg);
		$sp = explode(' ', $msg);
		$com = substr(array_shift($sp), 1);
		$args = implode(' ', $sp);
		return array($com, $args);
	}

	/**
	 * Parse a message from the server
	 *
	 * @todo Rewrite this, as it is taken from PHP:IRC
	 * @param string $response
	 * @return array
	 */
	private function ParseResponse($response)
	{
		$message = explode(' ', $response, 2);
		if ($message[0][0] == ':') {
			// parse prefix
			$prefix = substr($message[0], 1);
			if (strpos($prefix, '!') !== false) {
				list($origin, $orighost) = explode('!', $prefix, 2);
			} else {
				$origin   = $prefix;
				$orighost = null;
			}
			list($command, $rest) = explode(' ', $message[1], 2);
			// foo :bar
			if (strpos($rest, ' :') !== false) {
				list($target, $params) = explode(' :', $rest, 2);
				// :bar
			} elseif ($rest[0] == ':') {
				$target = substr($rest, 1);
				$params = null;
				// foo
			} else {
				$target = $rest;
				$params = null;
			}
		// Server messages (PING, NOTICE, ERROR)
		} else {
			$origin   = null;
			$orighost = null;
			$command  = $message[0];
			if (strpos($message[1], ' :') !== false) {
				list($target, $params) = explode(' :', $message[1], 2);
			} else {
				$target = null;
				$params = substr($message[1], 1);
			}
		}
		return array('COM' => $command, 'ORI' => $origin, 'HOST' => $orighost, 'TAR' => $target, 'PARAM' => $params);
	}
}
?>